Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[fix] File upload #850

Merged
merged 15 commits into from
Aug 29, 2024
Merged

[fix] File upload #850

merged 15 commits into from
Aug 29, 2024

Conversation

cjpais
Copy link
Contributor

@cjpais cjpais commented Aug 26, 2024

I have a simple HTML form which submits a file to a server action via multipart form upload. From the client side, the file looks proper, but when the server action receives it, the file is much larger? Is there some intermediate processing of this file or improper handling in the library?

client sent:

File {
  lastModified: 1724514738000
  name: "bara.jpeg"
  size: 2094593
  type: "image/jpeg"
}

server received:

File {
  size: 3715003,
  type: 'image/jpeg',
  name: 'bara.jpeg',
  lastModified: 1724698582443
}

I went through the code, and the bug largely was parsing the body as a string. Instead we have an array buffer so we can manipulate it more directly. It's possible it may still work as a string, but I believe the array buffer leaves more flexibility.

I've only tested directly in the application I've been building

Copy link

vercel bot commented Aug 26, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Updated (UTC)
waku ✅ Ready (Inspect) Visit Preview Aug 29, 2024 0:00am

Copy link

codesandbox-ci bot commented Aug 26, 2024

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

@cjpais
Copy link
Contributor Author

cjpais commented Aug 26, 2024

i would guess the right thing to do is probably move to busboy, etc

Copy link
Owner

@dai-shi dai-shi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch and this looks great. Thanks for working on it.

@@ -1,8 +1,16 @@
// TODO is this correct? better to use a library?
export const parseFormData = (body: string, contentType: string) => {
export const parseFormData = (body: ArrayBuffer, contentType: string) => {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you be able to create a test for this function?

Copy link
Contributor Author

@cjpais cjpais Aug 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will try, going on vacation soon so have a lot of other projects to wrap up

i've only minorly tested the code itself, but it appears to be working in the project i found the bug in

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looking forward to it. take your time.

@@ -194,13 +194,14 @@ export async function renderRsc(

let decodedBody: unknown | undefined = args.decodedBody;
if (body) {
const bodyStr = await streamToString(body);
const bodyBuf = await streamToArrayBuffer(body);
const bodyStr = arrayBufferToString(bodyBuf);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can delay calling this in the else-if block. We just need to check if the buffer has a length.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great, latest version should have this

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, thanks.

@dai-shi
Copy link
Owner

dai-shi commented Aug 26, 2024

i would guess the right thing to do is probably move to busboy, etc

We used to use busboy as React lib uses it, but it's Node.js specific and Waku now supports non-Node.js envs.

@cjpais
Copy link
Contributor Author

cjpais commented Aug 27, 2024

tests are a good starting point, but really need more. large files, etc.. the current code does not pass. the tests themselves need some review for accuracy as well

Copy link
Owner

@dai-shi dai-shi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, those tests are good start. Thanks for working on it. It's hard to tell the accuracy, but it's noticeable.

2nd review 👇 . Maybe we need a test for arrayBufferToString.

@@ -31,6 +56,13 @@ export const streamToString = async (
return outs.join('');
};

export function arrayBufferToString(buffer: ArrayBuffer): string {
const uint8Array = new Uint8Array(buffer);
return Array.from(uint8Array)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not familiar but does it create a new array?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

array.from does yes, I think we should try to avoid conversions if possible, or convert once and handle from there. this feels like a hack to me

export function arrayBufferToString(buffer: ArrayBuffer): string {
const uint8Array = new Uint8Array(buffer);
return Array.from(uint8Array)
.map((byte) => String.fromCharCode(byte))
Copy link
Owner

@dai-shi dai-shi Aug 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem to work for non-ASCII chars. Don't we need to use TextDecoder?

Do we have the same issue in parseFormData?

Copy link
Contributor Author

@cjpais cjpais Aug 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep I believe so

@dai-shi dai-shi mentioned this pull request Aug 27, 2024
@cjpais
Copy link
Contributor Author

cjpais commented Aug 27, 2024

So I initially wrote this code as a hack to solve a specific issue for my project. I think after writing the tests and seeing some of the review comments, it might be nice to review this body of code in total and maybe write a better implementation to handle more cases (like binary, base64, quoted strings, etc).

Mentioned in discord but I see Hono is doing this. Not sure if applicable given support for non node.js based runtimes, I'm not super familiar with the API's down to that level. But since Hono supports different runtimes, I imagine there are either polyfills or something to get the parsing working.

export const bufferToFormData = (
  arrayBuffer: ArrayBuffer,
  contentType: string
): Promise<FormData> => {
  const response = new Response(arrayBuffer, {
    headers: {
      'Content-Type': contentType,
    },
  })
  return response.formData()
}

if I get a chance today I may try something as simple as this. (however I am using bun runtime so it probably has support for the node.js apis)

Edit: I couldn't help myself from trying. It looks to have worked, but still failing some tests (could be poorly written tests? as this is a canonical implementation??). If this is a valid way to go it is by far the simplest and probably most robust. I am going to submit the code I have shortly, as well as updated arrayBufferToString from Hono as well

@@ -56,13 +56,6 @@ export const streamToString = async (
return outs.join('');
};

export function arrayBufferToString(buffer: ArrayBuffer): string {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hono provides this now

Copy link
Owner

@dai-shi dai-shi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall looks good. Thanks for working on it.

Copy link
Owner

@dai-shi dai-shi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is so great! Thanks for your contribution!

@dai-shi dai-shi merged commit 9c5a655 into dai-shi:main Aug 29, 2024
28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants